package com.solution.util;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import org.hibernate.Criteria;
import org.hibernate.criterion.MatchMode;
import org.hibernate.criterion.Restrictions;

/**
 * This class is designed to help in searching records in database. users can
 * add search conditions by using different methods. Methods for adding
 * conditions are in pairs. one method with a boolean flag and other without it.
 * Boolean flag must be set to true of property (mentioned for searching) is
 * looking into component inside the class. Else, it will be considered as a
 * member variable. Note that search on basis of components of member variables
 * is NOT supported.
 * 
 * @author ashok.agrawal
 * 
 */

public class SearchVO implements Serializable {

	/**
	 * 
	 */
	private static final long serialVersionUID = 5699481852902819161L;

	private Set<Condition> conditions;
	private List<Order> orders;
	private int fetchSize;
	private Integer pageNumber;
	private Integer pageSize;

	public void setFetchSize(int fetchSize) {
		if(fetchSize > 0)
			this.fetchSize = fetchSize;
	}

	public void setPageNumber(Integer pageNumber) {
		this.pageNumber = pageNumber;
	}

	public void setPageSize(Integer pageSize) {
		this.pageSize = pageSize;
	}

	public void addEqualsCondition(String searchProperty, Object searchValue) {
		addEqualsCondition(searchProperty, searchValue, false);
	}

	public void addEqualsCondition(String searchProperty, Object searchValue,
			boolean isComponent) {
		if (searchValue != null && searchProperty != null
				&& searchProperty.length() > 0 && !searchProperty.endsWith("."))
			addCondition(new Condition(searchProperty, searchValue,
					Condition.EQUALS, isComponent));
	}

	public void addNotEqualsCondition(String searchProperty, Object searchValue) {
		addNotEqualsCondition(searchProperty, searchValue, false);
	}

	public void addNotEqualsCondition(String searchProperty,
			Object searchValue, boolean isComponent) {
		if (searchValue != null && searchProperty != null
				&& searchProperty.length() > 0 && !searchProperty.endsWith("."))
			addCondition(new Condition(searchProperty, searchValue,
					Condition.NOT_EQUALS, isComponent));
	}

	public void addIsNullCondition(String searchProperty) {
		addIsNullCondition(searchProperty, false);
	}

	public void addIsNullCondition(String searchProperty, boolean isComponent) {
		if (searchProperty != null && searchProperty.length() > 0
				&& !searchProperty.endsWith("."))
			addCondition(new Condition(searchProperty, null, Condition.IS_NULL,
					isComponent));
	}

	public void addIsNotNullCondition(String searchProperty) {
		addIsNotNullCondition(searchProperty, false);
	}

	public void addIsNotNullCondition(String searchProperty, boolean isComponent) {
		if (searchProperty != null && searchProperty.length() > 0
				&& !searchProperty.endsWith("."))
			addCondition(new Condition(searchProperty, null,
					Condition.IS_NOT_NULL, isComponent));
	}

	public void addLesserCondition(String searchProperty, Object searchValue) {
		addLesserCondition(searchProperty, searchValue, false);
	}

	public void addLesserCondition(String searchProperty, Object searchValue,
			boolean isComponent) {
		if (searchValue != null && searchProperty != null
				&& searchProperty.length() > 0 && !searchProperty.endsWith("."))
			addCondition(new Condition(searchProperty, searchValue, Condition.LESSER,
					isComponent));
	}

	public void addGreaterCondition(String searchProperty, Object searchValue) {
		addGreaterCondition(searchProperty, searchValue, false);
	}

	public void addGreaterCondition(String searchProperty, Object searchValue,
			boolean isComponent) {
		if (searchValue != null && searchProperty != null
				&& searchProperty.length() > 0 && !searchProperty.endsWith("."))
			addCondition(new Condition(searchProperty, searchValue,
					Condition.GREATER, isComponent));
	}

	public void addLesserEqualsCondition(String searchProperty,
			Object searchValue) {
		addLesserEqualsCondition(searchProperty, searchValue, false);
	}

	public void addLesserEqualsCondition(String searchProperty,
			Object searchValue, boolean isComponent) {
		if (searchValue != null && searchProperty != null
				&& searchProperty.length() > 0 && !searchProperty.endsWith("."))
			addCondition(new Condition(searchProperty, searchValue,
					Condition.LESSER_EQUAL, isComponent));
	}

	public void addGreaterEqualsCondition(String searchProperty,
			Object searchValue) {
		addGreaterEqualsCondition(searchProperty, searchValue, false);
	}

	public void addGreaterEqualsCondition(String searchProperty,
			Object searchValue, boolean isComponent) {
		if (searchValue != null && searchProperty != null
				&& searchProperty.length() > 0 && !searchProperty.endsWith("."))
			addCondition(new Condition(searchProperty, searchValue,
					Condition.GREATER_EQUAL, isComponent));
	}

	public void addLikeCondition(String searchProperty, String searchValue) {
		addLikeCondition(searchProperty, searchValue, false);
	}

	public void addLikeCondition(String searchProperty, String searchValue,
			boolean isComponent) {
		if (searchValue != null && searchProperty != null
				&& searchProperty.length() > 0 && !searchProperty.endsWith("."))
			addCondition(new Condition(searchProperty, searchValue,
					Condition.LIKE, isComponent));
	}

	public void addLikeIgnoreCaseCondition(String searchProperty,
			String searchValue) {
		addLikeIgnoreCaseCondition(searchProperty, searchValue, false);
	}

	public void addLikeIgnoreCaseCondition(String searchProperty,
			String searchValue, boolean isComponent) {
		if (searchValue != null && searchProperty != null
				&& searchProperty.length() > 0 && !searchProperty.endsWith("."))
			addCondition(new Condition(searchProperty, searchValue,
					Condition.LIKE_IGNORE_CASE, isComponent));
	}

	public void addStartsWithCondition(String searchProperty, String searchValue) {
		addStartsWithCondition(searchProperty, searchValue, false);
	}

	public void addStartsWithCondition(String searchProperty,
			String searchValue, boolean isComponent) {
		if (searchValue != null && searchProperty != null
				&& searchProperty.length() > 0 && !searchProperty.endsWith("."))
			addCondition(new Condition(searchProperty, searchValue,
					Condition.LIKE_STARTS_WITH, isComponent));
	}

	public void addStartsWithIgnoreCaseCondition(String searchProperty,
			String searchValue) {
		addStartsWithIgnoreCaseCondition(searchProperty, searchValue, false);
	}

	public void addStartsWithIgnoreCaseCondition(String searchProperty,
			String searchValue, boolean isComponent) {
		if (searchValue != null && searchProperty != null
				&& searchProperty.length() > 0 && !searchProperty.endsWith("."))
			addCondition(new Condition(searchProperty, searchValue,
					Condition.LIKE_STARTS_WITH_IGNORE_CASE, isComponent));
	}

	public void addEqualsIgnoreCaseCondition(String searchProperty,
			String searchValue) {
		addEqualsIgnoreCaseCondition(searchProperty, searchValue, false);
	}

	public void addEqualsIgnoreCaseCondition(String searchProperty,
			String searchValue, boolean isComponent) {
		if (searchValue != null && searchProperty != null
				&& searchProperty.length() > 0 && !searchProperty.endsWith("."))
			addCondition(new Condition(searchProperty, searchValue,
					Condition.EQUALS_IGNORE_CASE, isComponent));
	}

	public void addEqualsCaseSensitiveCondition(String searchProperty,
			String searchValue) {
		addEqualsCaseSensitiveCondition(searchProperty, searchValue, false);
	}

	public void addEqualsCaseSensitiveCondition(String searchProperty,
			String searchValue, boolean isComponent) {
		if (searchValue != null && searchProperty != null
				&& searchProperty.length() > 0 && !searchProperty.endsWith("."))
			addCondition(new Condition(searchProperty, searchValue,
					Condition.EQUALS_CASE_SENSITIVE, isComponent));
	}

	public void addInCondition(String searchProperty, Collection searchValues) {
		addInCondition(searchProperty, searchValues, false);
	}

	public void addInCondition(String searchProperty, Collection searchValues,
			boolean isComponent) {
		if (searchValues != null && searchProperty != null
				&& searchProperty.length() > 0 && !searchProperty.endsWith("."))
			addCondition(new Condition(searchProperty, searchValues.toArray(),
					Condition.IN, isComponent));
	}

	public void addInCondition(String searchProperty, Object... searchValues) {
		addInCondition(searchProperty, false, searchValues);
	}

	public void addInCondition(String searchProperty, boolean isComponent,
			Object... searchValues) {
		if (searchValues != null && searchProperty != null
				&& searchProperty.length() > 0 && !searchProperty.endsWith("."))
			addCondition(new Condition(searchProperty, searchValues,
					Condition.IN, isComponent));
	}
	
	public void addNotInCondition(String searchProperty, Collection searchValues) {
		addNotInCondition(searchProperty, searchValues, false);
	}

	public void addNotInCondition(String searchProperty, Collection searchValues,
			boolean isComponent) {
		if (searchValues != null && searchProperty != null
				&& searchProperty.length() > 0 && !searchProperty.endsWith("."))
			addCondition(new Condition(searchProperty, searchValues.toArray(),
					Condition.NOT_IN, isComponent));
	}

	public void addNotInCondition(String searchProperty, Object... searchValues) {
		addNotInCondition(searchProperty, false, searchValues);
	}

	public void addNotInCondition(String searchProperty, boolean isComponent,
			Object... searchValues) {
		if (searchValues != null && searchProperty != null
				&& searchProperty.length() > 0 && !searchProperty.endsWith("."))
			addCondition(new Condition(searchProperty, searchValues,
					Condition.NOT_IN, isComponent));
	}
	
	public void addDateEqualsCondition(String searchProperty,
			Date searchValue){
		addDateEqualsCondition(searchProperty, searchValue, false);
	}
	
	public void addDateEqualsCondition(String searchProperty,
			Date searchValue, boolean isComponent){
		Date referenceDate = truncate(searchValue);
		addGreaterEqualsCondition(searchProperty, referenceDate, isComponent);
		addLesserCondition(searchProperty, new Date(referenceDate.getTime() + 86400000), isComponent);
	}

	public void addOrderAscending(String property) {
		if (property != null && property.length() > 0
				&& !property.endsWith("."))
			addOrder(new Order(property, Order.ASCENDING));
	}

	public void addOrderDescending(String property) {
		if (property != null && property.length() > 0
				&& !property.endsWith("."))
			addOrder(new Order(property, Order.DESCENDING));
	}

	private void addCondition(Condition condition) {
		if (conditions != null) {
			conditions.add(condition);
		} else {
			conditions = new TreeSet<Condition>();
			conditions.add(condition);
		}
	}

	private void addOrder(Order order) {
		if (orders != null) {
			orders.add(order);
		} else {
			orders = new ArrayList<Order>(3);
			orders.add(order);
		}
	}

	public Criteria buildCriteria(Criteria criterion) {
		if (conditions != null && conditions.size() > 0 && criterion != null) {
			// These two variables are for storing criteria name and criteria
			// used in last recursion of for loop
			String currentCriteriaName = "";
			Criteria currentCriteria = null;
			// These two variables store criteria name and criteria to be used
			// in this recursion of for loop
			String criteriaName = "";
			Criteria criteria = null;

			String searchProperty;
			Map<String, Criteria> criteriaMap = new HashMap<String, Criteria>();
			for (Condition condition : conditions) {
				if (condition.isComponent
						|| condition.searchProperty.indexOf('.') == -1) {
					criteria = criterion;
					criteriaName = "";
					searchProperty = condition.searchProperty;
				} else {
					criteriaName = condition.searchProperty.substring(0,
							condition.searchProperty.lastIndexOf('.'));
					if (!criteriaName.equals(currentCriteriaName)) {
						criteria = createCriteria(criterion, criteriaName,
								criteriaMap);
					} else {
						criteria = currentCriteria;
					}
					searchProperty = condition.searchProperty
							.substring(condition.searchProperty
									.lastIndexOf('.') + 1);
				}
				currentCriteriaName = criteriaName;
				currentCriteria = criteria;
				switch (condition.mode) {
				case Condition.EQUALS:
					criteria.add(Restrictions.eq(searchProperty,
							condition.searchValue));
					break;
				case Condition.GREATER:
					criteria.add(Restrictions.gt(searchProperty,
							condition.searchValue));
					break;
				case Condition.LESSER:
					criteria.add(Restrictions.lt(searchProperty,
							condition.searchValue));
					break;
				case Condition.LESSER_EQUAL:
					criteria.add(Restrictions.le(searchProperty,
							condition.searchValue));
					break;
				case Condition.GREATER_EQUAL:
					criteria.add(Restrictions.ge(searchProperty,
							condition.searchValue));
					break;
				case Condition.LIKE:
					criteria
							.add(Restrictions.like(searchProperty,
									(String) condition.searchValue,
									MatchMode.ANYWHERE));
					break;
				case Condition.LIKE_IGNORE_CASE:
					criteria
							.add(Restrictions.ilike(searchProperty,
									(String) condition.searchValue,
									MatchMode.ANYWHERE));
					break;
				case Condition.LIKE_STARTS_WITH:
					criteria.add(Restrictions.like(searchProperty,
							(String) condition.searchValue, MatchMode.START));
					break;
				case Condition.LIKE_STARTS_WITH_IGNORE_CASE:
					criteria.add(Restrictions.ilike(searchProperty,
							(String) condition.searchValue, MatchMode.START));
					break;
				case Condition.EQUALS_IGNORE_CASE:
					criteria.add(Restrictions.ilike(searchProperty,
							(String) condition.searchValue, MatchMode.EXACT));
					break;
				case Condition.EQUALS_CASE_SENSITIVE:
					criteria.add(Restrictions.like(searchProperty,
							(String) condition.searchValue, MatchMode.EXACT));
					break;
				case Condition.IN:
					criteria.add(Restrictions.in(searchProperty,
							(Object[]) condition.searchValue));
					break;
				case Condition.NOT_IN:
					criteria.add(Restrictions.not(Restrictions.in(searchProperty,
							(Object[]) condition.searchValue)));
					break;
				case Condition.NOT_EQUALS:
					criteria.add(Restrictions.ne(searchProperty,
							condition.searchValue));
					break;
				case Condition.IS_NULL:
					criteria.add(Restrictions.isNull(searchProperty));
					break;
				case Condition.IS_NOT_NULL:
					criteria.add(Restrictions.isNotNull(searchProperty));
					break;
				}
			}
		}
		if (orders != null && orders.size() > 0 && criterion != null) {
			for (Order order : orders) {
				if (order.type == Order.ASCENDING) {
					criterion.addOrder(org.hibernate.criterion.Order
							.asc(order.property));
				} else {
					criterion.addOrder(org.hibernate.criterion.Order
							.desc(order.property));
				}
			}
		}
		if(fetchSize > 0){
			criterion.setMaxResults(fetchSize);
		}
		if(pageNumber!=null && pageSize !=null && pageNumber > 0 && pageSize > 0){
			criterion.setFirstResult(pageNumber.intValue()*pageSize.intValue());
			criterion.setMaxResults(pageSize+1);
		}
		return criterion;
	}

	private Criteria createCriteria(Criteria currentCriteria,
			String memberName, Map<String, Criteria> criteriaMap) {
		int startIndex = 0;
		int nextIndex = memberName.indexOf('.');
		String key = "";
		String keyIncrement;
		boolean searchInMap = true;
		while (nextIndex > 0) {
			keyIncrement = memberName.substring(startIndex, nextIndex);
			key += keyIncrement;
			if (searchInMap && criteriaMap.get(key) != null) {
				currentCriteria = criteriaMap.get(key);
			} else {
				currentCriteria = currentCriteria.createCriteria(keyIncrement);
				criteriaMap.put(key, currentCriteria);
				searchInMap = false;
			}
			startIndex = nextIndex + 1;
			nextIndex = memberName.indexOf('.', startIndex);
			key += '.';
		}
		keyIncrement = memberName.substring(startIndex);
		key += keyIncrement;
		if (searchInMap && criteriaMap.get(key) != null) {
			currentCriteria = criteriaMap.get(key);
		} else {
			currentCriteria = currentCriteria.createCriteria(keyIncrement);
			criteriaMap.put(key, currentCriteria);
		}
		return currentCriteria;
	}
	
	/* Method for clearing instance variables, if required
	 * Can also be utilized for reusing the instance for next search
	 */
	public void clear(){
		conditions = null;
		orders = null;
		fetchSize = 0;
		pageNumber = null;
		pageSize = null;
	}
	
	public void clearConditions(){
		conditions = null;
	}
	public void clearOrders(){
		orders = null;
	}
	public void clearFetchSize(){
		fetchSize = 0;
	}
	public void clearPageNumber(){
		pageNumber = 0;
	}
	public void clearPageSize(){
		pageSize = 0;
	}
	
	private static Date truncate(Date input){
		if(input == null) return null;
		Calendar calendar = Calendar.getInstance();
		calendar.setLenient(false);
		calendar.setTime(input);
		calendar.set(Calendar.HOUR_OF_DAY, 0);
		calendar.set(Calendar.MINUTE, 0);
		calendar.set(Calendar.SECOND, 0);
		calendar.set(Calendar.MILLISECOND, 0);
		return calendar.getTime();
	}

	private class Condition implements Serializable, Comparable<Condition> {
		/**
		 * 
		 */
		private static final long serialVersionUID = -4556856799850116963L;
		private static final byte EQUALS = 1;
		private static final byte GREATER = 2;
		private static final byte LESSER = 3;
		private static final byte LESSER_EQUAL = 4;
		private static final byte GREATER_EQUAL = 5;
		private static final byte LIKE = 6;
		private static final byte LIKE_IGNORE_CASE = 7;
		private static final byte LIKE_STARTS_WITH = 8;
		private static final byte LIKE_STARTS_WITH_IGNORE_CASE = 9;
		private static final byte EQUALS_IGNORE_CASE = 10;
		private static final byte EQUALS_CASE_SENSITIVE = 11;
		private static final byte IN = 12;
		private static final byte NOT_IN = 13;
		private static final byte NOT_EQUALS = 14;
		private static final byte IS_NULL = 15;
		private static final byte IS_NOT_NULL = 16;

		private byte mode;
		private String searchProperty;
		private Object searchValue;
		private boolean isComponent;

		private Condition(String searchProperty, Object searchValue, byte mode) {
			this.searchProperty = searchProperty;
			this.searchValue = searchValue;
			this.mode = mode;
		}

		private Condition(String searchProperty, Object searchValue, byte mode,
				boolean isComponent) {
			this.searchProperty = searchProperty;
			this.searchValue = searchValue;
			this.mode = mode;
			this.isComponent = isComponent;
		}

		public int hashCode() {
			return searchProperty.hashCode() + searchValue.hashCode() + mode;
		}

		public boolean equals(Object other) {
			if (this == other)
				return true;
			if (other == null)
				return false;
			if (!this.getClass().equals(other.getClass()))
				return false;
			Condition c = (Condition) other;
			return this.mode == c.mode
					&& this.searchProperty.equals(c.searchProperty)
					&& this.searchValue.equals(c.searchValue);
		}

		public int compareTo(Condition o) {
			int result = this.searchProperty.compareTo(o.searchProperty);
			if (result != 0)
				return result;
			result = this.mode - o.mode;
			if (result != 0)
				return result;
			if (this.searchValue instanceof Comparable
					&& this.searchValue.getClass().equals(
							o.searchValue.getClass()))
				return ((Comparable) this.searchValue).compareTo(o.searchValue);
			if (this.searchValue.equals(o.searchValue))
				return 0;
			return 1;
		}
	}

	private class Order implements Serializable {
		/**
		 * 
		 */
		private static final long serialVersionUID = -8742403903278798684L;
		private static final byte ASCENDING = 1;
		private static final byte DESCENDING = 2;

		private byte type;
		private String property;

		private Order(String property, byte type) {
			this.property = property;
			this.type = type;
		}
	}
}
